// Sources/SwiftProtobuf/Google_Protobuf_Any+Extensions.swift - Well-known Any type
//
// Copyright (c) 2014 - 2017 Apple Inc. and the project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See LICENSE.txt for license information:
// https://github.com/apple/swift-protobuf/blob/main/LICENSE.txt
//
// -----------------------------------------------------------------------------
///
/// Extends the `Google_Protobuf_Any` type with various custom behaviors.
///
// -----------------------------------------------------------------------------

// Explicit import of Foundation is necessary on Linux,
// don't remove unless obsolete on all platforms
import Foundation

public let defaultAnyTypeURLPrefix: String = "type.googleapis.com"

extension Google_Protobuf_Any {
    /// Initialize an Any object from the provided message.
    ///
    /// This corresponds to the `pack` operation in the C++ API.
    ///
    /// Unlike the C++ implementation, the message is not immediately
    /// serialized; it is merely stored until the Any object itself
    /// needs to be serialized.  This design avoids unnecessary
    /// decoding/recoding when writing JSON format.
    ///
    /// - Parameters:
    ///   - partial: If `false` (the default), this method will check
    ///     ``Message/isInitialized-6abgi`` before encoding to verify that all required
    ///     fields are present. If any are missing, this method throws
    ///     ``BinaryEncodingError/missingRequiredFields``.
    ///   - typePrefix: The prefix to be used when building the `type_url`.
    ///     Defaults to "type.googleapis.com".
    /// - Throws: ``BinaryEncodingError/missingRequiredFields`` if
    /// `partial` is false and `message` wasn't fully initialized.
    public init(
        message: any Message,
        partial: Bool = false,
        typePrefix: String = defaultAnyTypeURLPrefix
    ) throws {
        if !partial && !message.isInitialized {
            throw BinaryEncodingError.missingRequiredFields
        }
        self.init()
        typeURL = buildTypeURL(forMessage: message, typePrefix: typePrefix)
        _storage.state = .message(message)
    }

    /// Creates a new `Google_Protobuf_Any` by decoding the given string
    /// containing a serialized message in Protocol Buffer text format.
    ///
    /// - Parameters:
    ///   - textFormatString: The text format string to decode.
    ///   - extensions: An `ExtensionMap` used to look up and decode any
    ///     extensions in this message or messages nested within this message's
    ///     fields.
    /// - Throws: an instance of `TextFormatDecodingError` on failure.
    @_disfavoredOverload
    public init(
        textFormatString: String,
        extensions: (any ExtensionMap)? = nil
    ) throws {
        // TODO: Remove this api and default the options instead when we do a major release.
        try self.init(
            textFormatString: textFormatString,
            options: TextFormatDecodingOptions(),
            extensions: extensions
        )
    }

    /// Creates a new `Google_Protobuf_Any` by decoding the given string
    /// containing a serialized message in Protocol Buffer text format.
    ///
    /// - Parameters:
    ///   - textFormatString: The text format string to decode.
    ///   - options: The ``TextFormatDecodingOptions`` to use.
    ///   - extensions: An ``ExtensionMap`` used to look up and decode any
    ///     extensions in this message or messages nested within this message's
    ///     fields.
    /// - Throws: ``TextFormatDecodingError`` on failure.
    public init(
        textFormatString: String,
        options: TextFormatDecodingOptions = TextFormatDecodingOptions(),
        extensions: (any ExtensionMap)? = nil
    ) throws {
        self.init()
        if !textFormatString.isEmpty {
            if let data = textFormatString.data(using: String.Encoding.utf8) {
                try data.withUnsafeBytes { (body: UnsafeRawBufferPointer) in
                    if let baseAddress = body.baseAddress, body.count > 0 {
                        var textDecoder = try TextFormatDecoder(
                            messageType: Google_Protobuf_Any.self,
                            utf8Pointer: baseAddress,
                            count: body.count,
                            options: options,
                            extensions: extensions
                        )
                        try decodeTextFormat(decoder: &textDecoder)
                        if !textDecoder.complete {
                            throw TextFormatDecodingError.trailingGarbage
                        }
                    }
                }
            }
        }
    }

    /// Returns true if this `Google_Protobuf_Any` message contains the given
    /// message type.
    ///
    /// The check is performed by looking at the passed `Message.Type` and the
    /// `typeURL` of this message.
    ///
    /// - Parameter type: The concrete message type.
    /// - Returns: True if the receiver contains the given message type.
    public func isA<M: Message>(_ type: M.Type) -> Bool {
        _storage.isA(type)
    }

    public func hash(into hasher: inout Hasher) {
        _storage.hash(into: &hasher)
    }
}

extension Google_Protobuf_Any {
    internal func textTraverse(visitor: inout TextFormatEncodingVisitor) {
        _storage.textTraverse(visitor: &visitor)
        try! unknownFields.traverse(visitor: &visitor)
    }
}

extension Google_Protobuf_Any {
    // Custom text format decoding support for Any objects.
    // (Note: This is not a part of any protocol; it's invoked
    // directly from TextFormatDecoder whenever it sees an attempt
    // to decode an Any object)
    internal mutating func decodeTextFormat(
        decoder: inout TextFormatDecoder
    ) throws {
        // First, check if this uses the "verbose" Any encoding.
        // If it does, and we have the type available, we can
        // eagerly decode the contained Message object.
        if let url = try decoder.scanner.nextOptionalAnyURL() {
            try _uniqueStorage().decodeTextFormat(typeURL: url, decoder: &decoder)
        } else {
            // This is not using the specialized encoding, so we can use the
            // standard path to decode the binary value.
            // First, clear the fields so we don't waste time re-serializing
            // the previous contents as this instances get replaced with a
            // new value (can happen when a field name/number is repeated in
            // the TextFormat input).
            self.typeURL = ""
            self.value = Data()
            try decodeMessage(decoder: &decoder)
        }
    }
}

extension Google_Protobuf_Any: _CustomJSONCodable {
    internal func encodedJSONString(options: JSONEncodingOptions) throws -> String {
        try _storage.encodedJSONString(options: options)
    }

    internal mutating func decodeJSON(from decoder: inout JSONDecoder) throws {
        try _uniqueStorage().decodeJSON(from: &decoder)
    }
}
